package nachos.network;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Set;

import nachos.machine.*;
import nachos.threads.*;

/**
 * A collection of message queues, one for each local port. A
 * <tt>PostOffice</tt> interacts directly with the network hardware. Because
 * of the network hardware, we are guaranteed that messages will never be
 * corrupted, but they might get lost.
 *
 * <p>
 * The post office uses a "postal worker" thread to wait for messages to arrive
 * from the network and to place them in the appropriate queues. This cannot
 * be done in the receive interrupt handler because each queue (implemented
 * with a <tt>SynchList</tt>) is protected by a lock.
 */
public class TransportOffice {
    /**
     * Allocate a new post office, using an array of <tt>SynchList</tt>s.
     * Register the interrupt handlers with the network hardware and start the
     * "postal worker" thread.
     */
    public TransportOffice() {
	messageReceived = new Semaphore(0);
	messageSent = new Semaphore(0);
	sendLock = new Lock();
	receiveLock = new Lock();
	UNACKED = new HashSet<EnhancedPacket>();
	replies = new HashMap<Socket,EnhancedPacket>();
	
	receivebox = new ExtendedSynchList[EnhancedPacket.portLimit];
	sendOutbox = new ExtendedSynchList[EnhancedPacket.portLimit];
	slidingWindow = new int[EnhancedPacket.portLimit];
	
	
	queues = new SynchList[EnhancedPacket.portLimit];
	for(int i = 0; i <sendOutbox.length; i++)
		 sendOutbox[i] = new ExtendedSynchList();
	
	for (int i=0; i<receivebox.length; i++){
	    queues[i] = new SynchList();
	    receivebox[i] = new ExtendedSynchList();
	    slidingWindow[i] = 16;
	}
	
	Runnable receiveHandler = new Runnable() {
	    public void run() { receiveInterrupt(); }
	};
	Runnable sendHandler = new Runnable() {
	    public void run() { sendInterrupt(); }
	};
	//used so we can interrupt with send() and receive()
	Machine.networkLink().setInterruptHandlers(receiveHandler,
						   sendHandler);

	KThread sendPacketThread = new KThread(new Runnable() {
		public void run() { transportSend(); }
	    });
	
	KThread receivePacketThread = new KThread(new Runnable() {
		public void run() { transportReceive(); }
	});

	KThread timeoutPacketThread = new KThread(new Runnable() {
		public void run() { transportTimeout(); }
	});

    sendPacketThread.fork();
	receivePacketThread.fork();
	timeoutPacketThread.fork();
	
    }

    /**
     * Retrieve a message on the specified port, waiting if necessary.
     *
     * @param	port	the port on which to wait for a message.
     *
     * @return	the message received.
     */
    public EnhancedPacket receive(int port) {
	Lib.assertTrue(port >= 0 && port < queues.length);

	Lib.debug(dbgNet, "waiting for mail on port " + port);

	EnhancedPacket mail = (EnhancedPacket) queues[port].removeFirst();

	if (Lib.test(dbgNet))
	    System.out.println("got mail on port " + port + ": " + mail);

	return mail;
    }

    /**
     * Wait for incoming messages, and then put them in the correct mailbox.
     */
    private void transportReceive() {
		while (true) {
			//System.out.println("OMGGGGGG");
			messageReceived.P();
		
		    Packet p = Machine.networkLink().receive();
		
		    EnhancedPacket mail;
		    try {
			mail = new EnhancedPacket(p);
		    }
		    catch (MalformedPacketException e) {
		    	System.out.println("Do I ever get here? (error)");
		    	continue;
		    }
		    if(receivebox[mail.dstPort].getSize() >= 16){
		    //	System.out.println("I AM FULL");
		    	//Lib.debug(dbgNet, "FULL");
		    	continue;
		    }
		   
		    if (Lib.test(dbgNet))
			System.out.println("delivering mail to port " + mail.dstPort
					   + ": " + mail);
		    System.out.println("CHECKING PACKET "+ mail.signal + " " + EnhancedPacket.packetType.ACK.getIntValue());
		  
		    //checks if it's an ACK
		    if(mail.signal == EnhancedPacket.packetType.ACK.getIntValue()){
		    	
		    	//makes a new copy for checking the packet
		    	EnhancedPacket packetCopy;
		    	try{
		    		packetCopy = new EnhancedPacket(mail.packet.srcLink, mail.srcPort, mail.packet.dstLink, mail.dstPort,
							false, false, false, false, mail.packetID, new byte[0]);
		    	}
		    	catch(MalformedPacketException e){
		    		System.out.println("ERROR");
		    		continue;
		    	}
		    	
				    	// comparing the current packet with the stored packets to check for duplicates
				    	EnhancedPacket checkPacket;
						HashSet removals = new HashSet<EnhancedPacket>();
						receiveLock.acquire();
							Iterator i = UNACKED.iterator();
							while (i.hasNext()) {
								checkPacket = (EnhancedPacket)i.next();
								if (packetCopy.equals(checkPacket)) {
									Lib.debug(dbgNet, "ACKED: " + packetCopy);
									slidingWindow[packetCopy.srcPort]++;
									Lib.debug(dbgNet, "sendBuffers["+packetCopy.srcPort+"] = "+ slidingWindow[packetCopy.srcPort]);
									removals.add(checkPacket);
								}
							}
							UNACKED.removeAll(removals);
						receiveLock.release();
						
		    }//if statement
		    else{
				receivebox[mail.dstPort].add(mail);
				EnhancedPacket ACK;
				try {//making an ACK PACKET
					ACK = new EnhancedPacket(mail.packet.srcLink, mail.srcPort, mail.packet.dstLink, mail.dstPort,
							false, false, true, false, mail.packetID, new byte[0]);
				}
				catch (MalformedPacketException e) {
					System.out.println("ERROR ON ACK! DROP TIME!");
					continue;
				}
				
				sendLock.acquire();
					Lib.debug(dbgNet, "ACKing message " + mail);
					Machine.networkLink().send(ACK.packet);
					messageSent.P();
				sendLock.release();
				
				if(mail.signal == 8 || mail.signal == 4)  //checks if it's a FIN packet or a STP packet
				{
					Socket newSocket = new Socket(mail.dstPort,mail.srcPort,p.srcLink,mail.srcPort);
					EnhancedPacket reply = replies.get(newSocket);
					if (reply != null) {
						if (mail.signal == 8) {
							System.out.println("I am already in FIN state, replying with an ACK");
							send(reply);
						}
						else {
							System.out.println("Not in FIN state, so turning into FIN state and sending an ACK");
							reply.signal = 8;
							send(reply);
						}
					}
				}
		    }
		    // atomically add message to the mailbox and wake a waiting thread
		    //queues[mail.dstPort].add(mail);
		}
    }
    
    private void transportSend(){
    	while (true) {
    		//System.out.println("ARE YOU LOOPING FOREVER???!?!?!");
			for (int i = 0; i < EnhancedPacket.portLimit - 1; i++) {
				//System.out.println("DO I EVER GET HERE?!?!??!");
				if (sendOutbox[i].getFirst() == null) {
					continue;
				}
				if (slidingWindow[i] <= 0) {
					Lib.debug(dbgNet, "Window doesn't exist at this port# " + i);
					continue;
				}
				EnhancedPacket p = (EnhancedPacket)sendOutbox[i].pop();
				//Lib.debug(dbgNet, "Sending: " + p);
				//System.out.println("Sending: " + p);
				sendLock.acquire();
				slidingWindow[i]--;
				//System.out.println( "sendBuffers["+p.srcPort+"] = "+slidingWindow[p.srcPort]);
			//	Lib.debug(dbgNet, "sendBuffers["+p.srcPort+"] = "+slidingWindow[p.srcPort]);
				Machine.networkLink().send(p.packet);
				messageSent.P();
				sendLock.release();
				receiveLock.acquire();
				UNACKED.add(p);
				receiveLock.release();
			}
    		//System.out.println("ARE YOU LOOPING FOREVER???!?!?!");
		}
    	
    }
    private void transportTimeout(){
		while(true){
			EnhancedPacket p = null;
			while (true) {
				ThreadedKernel.alarm.waitUntil(10000);
				receiveLock.acquire();
				/*
				for(EnhancedPacket t: UNACKED){
					Lib.debug(dbgNet, "Timeout: " + t);
					sendOutbox[t.dstPort].add(t);
					slidingWindow[t.dstPort]++;
					Lib.debug(dbgNet, "sendBuffers["+t.srcPort+"] = "+slidingWindow[t.srcPort]);
				}
				UNACKED.clear();
				receiveLock.release();
				
				*///the same thing as above right?
				
				Iterator i = UNACKED.iterator();
				while (i.hasNext()) {
					p = (EnhancedPacket)i.next();
				//	System.out.println("Timeout: " + p);
					//Lib.debug(dbgNet, "Timeout: " + p);
					sendOutbox[p.dstPort].add(p);
					slidingWindow[p.dstPort]++;
					//Lib.debug(dbgNet, "sendBuffers["+p.srcPort+"] = "+slidingWindow[p.srcPort]);
				}
				UNACKED.clear();
				receiveLock.release();
			}
			
			//System.out.println("AM I ASLEEP?");
			//ThreadedKernel.alarm.waitUntil(10000);
		}
    }
    

    
	//Putting it into a list of sent packets
	public void send(EnhancedPacket p) {
		Lib.debug(dbgNet, "Adding to the list of sendsOut: "+p);
		sendOutbox[p.srcPort].add(p);
	}
	
//	public void send(Packet p){
	//	Machine.networkLink().send(p);
	//}
	
	//receives a packet
	public Packet receivePacket(int srcPort, int host, int port){
		return Machine.networkLink().receive();
	}
    
    public EnhancedPacket receive(int srcPort, int hostLink, int port){
    	while (true) {
			EnhancedPacket p = (EnhancedPacket)receivebox[port].beginIterate();
			while (p != null) {
				if (p.packet.srcLink == hostLink && p.srcPort == srcPort) {
					receivebox[port].remove();
					System.out.println("GOT THE MESSAGE "+ p);
					//Lib.debug(dbgNet, "GOT THE MESSAGE: " + p);
					receivebox[port].endIterate();
					return p;
				}
				System.out.println("I WAS RECEIVING STUFF");
				p = (EnhancedPacket)receivebox[port].iterate();
			}
			receivebox[port].endIterate();
		}
    //	EnhancedPacket packet = null; //used as a temprory return
    //	return p;
    }
    
    
    
    /**
     * Called when a packet has arrived and can be dequeued from the network
     * link.
     */
    private void receiveInterrupt() {
	messageReceived.V();
    }

    /**
     * Send a message to a mailbox on a remote machine.
     */
  /*  public void send(EnhancedPacket ping) {
	if (Lib.test(dbgNet))
	    System.out.println("sending mail: " + ping);

	sendLock.acquire();

	Machine.networkLink().send(ping.packet);
	messageSent.P();

	sendLock.release();
    }
   */ 
    
    //a way to check if there are any current existing packet inside the storage and 
    //return that packet and remove it form the storage
	public EnhancedPacket getAndRemoveFirst(int port) {
		Lib.assertTrue(port >= 0 && port < receivebox.length); //need a way to stop without returning a value
		EnhancedPacket p = (EnhancedPacket)receivebox[port].getFirst();
		if (p != null) {
			receivebox[port].removeFirst();
			Lib.debug(dbgNet, "MESSAGE RECEIVED: " + p);
		}
		return p;
	}

    /**
     * Called when a packet has been sent and another can be queued to the
     * network link. Note that this is called even if the previous packet was
     * dropped.
     */
    private void sendInterrupt() {
	messageSent.V();

    }
    private int[] slidingWindow;
	public ExtendedSynchList[] receivebox; 
	public ExtendedSynchList[] sendOutbox;
    private SynchList[] queues;
    private Semaphore messageReceived;	// V'd when a message can be dequeued
    private Semaphore messageSent;	// V'd when a message can be queued
    private Lock sendLock, receiveLock;
    Set<EnhancedPacket> UNACKED;
	public HashMap<Socket, EnhancedPacket> replies;
    HashMap<String, Socket> activelyworkingSockets;
    ArrayList<Socket> waitingforconnectionSockets;
    private static final char dbgNet = 'n';
}
